插件化实现Android多主题功能原理剖析

前言

之前我们总结过B站的皮肤框架MagicaSakura,也点出了其不足,文章链接:来自B站的开源的MagicaSakura源码解析,该框架只能完成普通的换色需求,没有QQ,网易云音乐类似的皮肤包的功能。

那么今天我们就来看一款拥有皮肤加载功能的插件化换肤框架。其已经集成在我的应用https://github.com/Jerey-Jobs/KeepGank中。

这样做有两个好处:

  1. 皮肤可以不集成在apk中,减小apk体积
  2. 动态化增加皮肤,灵活性大,自由度很大

如何实现换肤功能

想当然的,在View创建的时候这是让我们应用能够完美的加载皮肤的最好方案。

那么我们知道,对于Activity来说,有一个可以复写的方法叫onCreateView

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return super.onCreateView(parent, name, context, attrs);
}

我们的view的创建就是通过这个方法来的,我们甚至可以通过复写这个方法,实现view的替换,比如本来要的是TextView,我们直接给它替换成Button.而这个方法其实是实现的LayoutInflaterFactory接口。

关于LayoutInflaterFactory,我们可以看一下鸿神的文章http://www.tuicool.com/articles/EVzEny6

创建View

根据拿到的onCreateView里面的name,来反射创建View,这边用到了一个技巧:onCreateView中的name,对于系统的View,是没有'.'符号的,比如"TextView"我们拿到的直接是TextView,
但是自定义的View,我们拿到的是带有包名的全部名称,因此反射时,对于系统的View,我们需要加上系统的包名,自定义的View,则直接使用name。

也不用疑问为什么用反射,这样不是慢吗?

因为系统的LayoutInflater在createView的时候也是这么做的,这边的代码都是参考系统的实现的。

    private static final String[] sClassPrefixList = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };
    static View createViewFromTag(Context context, String name, AttributeSet attrs) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        try {
            mConstructorArgs[0] = context;
            mConstructorArgs[1] = attrs;
            // 系统控件,没有".",因此去创建系统View
            if (-1 == name.indexOf('.')) {
                // 根据名称反射创建
                for (int i = 0; i < sClassPrefixList.length; i++) {
                    final View view = createView(context, name, sClassPrefixList[i]);
                    if (view != null) {
                        return view;
                    }
                }
                return null;
                // 有'.'的情况下是自定义View,V4与V7也会走
            } else {
                // 直接根据名称创建View
                return createView(context, name, null);
            }
        } catch (Exception e) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            // try
            return null;
        } finally {
            // Don't retain references on context.
            mConstructorArgs[0] = null;
            mConstructorArgs[1] = null;
        }
    }

    /**
 * 反射,使用View的两参数构造方法创建View
 * @param context
 * @param name
 * @param prefix
 * @return
 * @throws ClassNotFoundException
 * @throws InflateException
 */
private static View createView(Context context, String name, String prefix)
        throws ClassNotFoundException, InflateException {
    Constructor<? extends View> constructor = sConstructorMap.get(name);

    try {
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            Class<? extends View> clazz = context.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);

            constructor = clazz.getConstructor(sConstructorSignature);
            sConstructorMap.put(name, constructor);
        }
        constructor.setAccessible(true);
        return constructor.newInstance(mConstructorArgs);
    } catch (Exception e) {
        // We do not want to catch these, lets return null and let the actual LayoutInflater
        // try
        return null;
    }
}

判断View是否需要换肤

与创建View一样,根据拿到的onCreateView里面的AttributeSet attrs

拿到后,我们解析attrs

/**
 * 拿到attrName和value
 * 拿到的value是R.id
 */
String attrName = attrs.getAttributeName(i);//属性名
String attrValue = attrs.getAttributeValue(i);//属性值

根据属性名和属性值进行判断,有背景的属性,是否符合需要换肤的属性、

插件化资源注入

我们的皮肤包其实是APK,是我们写的另一个app,与正式App不同的是,其只有资源文件,且资源文件需要和主app同名。

1.通过 PackageManager拿皮肤包名
2.拿到皮肤包里面的Resource

但是因为我们想new Resources()时候,发现其第一个参数是AssetManager,但是AssetManager的构造方法在源码中被@hide了,我们没有方法拿到这个类,但是幸好其类还是能拿到的,我们直接反射获取。

我们拿资源的代码如下。

PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
/**
 * AssetManager assetManager = new AssetManager();
 * 这个方法被@ hide了。。我们只能通过反射newInstance
 */
AssetManager assetManager = AssetManager.class.newInstance();
/**
 * addAssetPath同样被系统给hide了
 */
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
/**
 * 讲皮肤路径保存,并设置不是默认皮肤
 */
SkinConfig.saveSkinPath(context, params[0]);
skinPath = skinPkgPath;
isDefaultSkin = false;
/**
 * 到此,我们拿到了外置皮肤包的资源
 */
return skinResource;

如何动态的从皮肤包中获取资源

我们以从皮肤包里面获取color来举例

业务端是通过资源的id来获取color的,资源的id也就是一个在编译时就生成的int型。 而皮肤包的也是编译时生成的,因此两个id是不一样的,我们只能通过资源的id先拿到在我们应用里的该id的名字,再通过名字去资源包里面拿资源。

public int getColor(int resId) {
    int originColor = ContextCompat.getColor(context, resId);
    /**
     * 如果皮肤资源包不存在,直接加载
     */
    if (mResources == null || isDefaultSkin) {
        return originColor;
    }
    /**
     * 每个皮肤包里面的id是不一样的,只能通过名字来拿,id值是不一样的。
     * 1. 获取默认资源的名称
     * 2. 根据名称从全局mResources里面获取值
     * 3. 若获取到了,则获取颜色返回,若获取不到,老老实实使用原来的
     */
    String resName = context.getResources().getResourceEntryName(resId);

    int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
    int trueColor;
    if (trueResId == 0) {
        trueColor = originColor;
    } else {
        trueColor = mResources.getColor(trueResId);
    }
    return trueColor;
}

实际使用

上面都是我们插件化加载的需要了解的知识,真的进行框架使用的时候,使用了自定义属性,根据自定义属性判断是否需要换肤。

使用观察者模式,所有需要换肤的view都会存放在Activity一个集合中,在皮肤管理器通知皮肤更新时,主动更新视图状态。

说了这么多了,框架的分装和使用具体可以看我的工程里面的代码。
https://github.com/Jerey-Jobs/KeepGank

效果如图:
细心的朋友会注意到,每个主题主页的左上角图片是会变的,没错,那个图片是动态加载的资源包里面的。

代码见:https://github.com/Jerey-Jobs/KeepGank

欢迎star

APK下载 App下载链接


本文作者:Anderson/Jerey_Jobs

博客地址 : http://jerey.cn/

简书地址 : Anderson大码渣

github地址 : https://github.com/Jerey-Jobs

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容

  • 前言: 本文主要讲述如何在项目中,在不重启应用的情况下,实现动态换肤的效果。换肤这块做的比较好的,有网易云音乐,q...
    Yagami3zZ阅读 13,360评论 5 50
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 今天再给大家带来一篇干货。 Android的主题换肤 ,可插件化提供皮肤包,无需Activity的重启直接实现无缝...
    _SOLID阅读 98,974评论 147 1,119
  • 我没想过自己会在此时此景下迫切的想要写些什么来疏解自己,有的时候会感激现在的自己,起码学会了写些不那么直接的文字,...
    芥子之人阅读 129评论 0 0
  • 今天周日。继昨天的超级忙碌之后,今天终于可以好好放松一下了。 今天早上睡到6点半才醒来,看了半小时视频,起床练了将...
    灵动的兰兰阅读 227评论 2 2